/*
 * DateUtils.java
 *
 * Created on 26 May 2006, 21:30
 *
 * Copyright (C)
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package com.grubby.utils;

import com.grubby.generators.RandomGenerators;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Static methods to perform common tasks relating to dates.
 * @author A. HARMEL-LAW. E: andrew.harmel.law@gmail.com
 * @version 0.1
 */
public final class DateUtils {
    
    public static final Logger LOGGER
            = Logger.getLogger("com.grubby.utils.DateUtils");
    
    /** Log Strings */
    private static final String NULL_PARAMETER_EX = "A parameter was null: ";
    
    /** Private Constructor */
    private DateUtils () { }
    
    /**
     * Returns the Date right now as a String in the form dd/MM/yyyy.
     * @return The Date right now as a String of format dd/MM/yyyy.
     */
    public static String getDateNowString() {
        
        // Get a dummy date (right now)
        Calendar myNowTime = Calendar.getInstance();
        
        // Create a simple date formatter
        SimpleDateFormat mySDF = new SimpleDateFormat("dd/MM/yyyy");
        
        // Return the time now as a date
        return mySDF.format(myNowTime.getTime());
        
    }
    
    /**
     * Returns a random Date of birth as a dd/MM/yyyy String.  The Date will
     * be between the maximum and minimum ages provided.
     * NOTE: This is not quite perfectly random but does the job.
     * @param minAge The minimum age required.
     * @param maxAge The maximum age required.
     * @return The date of birth in the format dd/MM/yyyy.
     * @throws IllegalArgumentException If minAge is greater than maxAge.
     */
    public static String getRandomDOB(int minAge, int maxAge) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandomDOB(minAge, maxAge, rand);
        
    }
    
    /**
     * Returns a random Date of birth as a dd/MM/yyyy String.  The Date will
     * be between the maximum and minimum ages provided.
     * <P>
     * Nullsafe - Throws an IllegalArgumentException if the Random object passed
     * as a parameter is null, or if the minAge is greater than the maxAge.
     * <P>
     * NOTE: This is not quite perfectly random but does the job.
     * @param minAge The minimum age required.
     * @param maxAge The maximum age required.
     * @param rand The seeded Random to use to generate the DOB Date.
     * @return The date of birth in the format dd/MM/yyyy.
     * @throws IllegalArgumentException If the Random object provided is NULL 
     * or the minAge is greater than the maxAge.
     */
    public static String getRandomDOB(int minAge, int maxAge, Random rand) {
        
        if (rand == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else if (minAge > maxAge) {
            
            LOGGER.log(Level.WARNING, "The age parameters were the wrong way " 
                    + "round");
            throw new IllegalArgumentException("The age parameters were the " 
                    + "wrong way round");
            
        } else if (minAge == maxAge) {
            
            return minAge + "";
            
        }
        
        // Get the Date today (right now)
        Calendar myRandomDOB = GregorianCalendar.getInstance();
        
        // Calc a random age between the requested max and min
        int age = rand.nextInt(maxAge - minAge) + minAge;
        
        // Set the time now back the required number of years
        myRandomDOB.set(Calendar.YEAR, myRandomDOB.get(Calendar.YEAR) - age);
        
        // Now scroll back month and day randomly too
        // NOTE: This is not perfect - the day rollback is based on a case
        // which always works (i.e. 28)
        int numMonthsBack = rand.nextInt(11);
        myRandomDOB.roll(Calendar.MONTH, -numMonthsBack);
        int numDaysBack = rand.nextInt(28);
        myRandomDOB.roll(Calendar.DAY_OF_MONTH, -numDaysBack);
        
        // Return the date as a formatted String
        SimpleDateFormat mySDF = new SimpleDateFormat("dd/MM/yyyy");
        return mySDF.format(myRandomDOB.getTime());
        
    }
    
    /**
     * Returns a random Date within the Date range provided.
     * <P>
     * Nullsafe - Throws an IllegalArgumentException if one of the arguments is
     * null or the start Date is after the end Date.
     * @param startDate The Date which the new Date must be equal to or after.
     * @param endDate The Date which the new Date must be equal to or before.
     * @return The random Date.
     * @throws IllegalArgumentException If the startDate or endDate object 
     * provided is NULL or if the startDate is after the endDate.
     */
    public static Date getRandomDateWithinRange(Date startDate, Date endDate) {
        
        Random rand = new Random(System.currentTimeMillis());
        return getRandomDateWithinRange(startDate, endDate, rand);
        
    }
    
    
    /**
     * Returns a random Date within the Date range provided.
     * <P>
     * Nullsafe - Throws an IllegalArgumentException if one of the arguments is
     * null or the start Date is after the end Date.
     * @param startDate The Date which the new Date must be equal to or after.
     * @param endDate The Date which the new Date must be equal to or before.
     * @param rand The seeded Random to use to get the random Date.
     * @return The random Date.
     * @throws IllegalArgumentException If the startDate, endDate or Random
     * object provided is NULL or if the startDate is after the endDate.
     */
    public static Date getRandomDateWithinRange(Date startDate, Date endDate,
            Random rand) {
        
        Date myDate;
        
        if (startDate == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "startDate");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "startDate");
            
        } else if (endDate == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "endDate");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "endDate");
            
        } else if (rand == null){
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "rand");
            throw new IllegalArgumentException(NULL_PARAMETER_EX + "rand");
            
        } else if (startDate.after(endDate)) {
            
            LOGGER.log(Level.WARNING, "The start date was after the end date. "
                    + "Throwning an IllegalArgumentException.");
            throw new IllegalArgumentException("The start date was after the " +
                    "end date. Throwning an IllegalArgumentException.");
            
        } else if (startDate.getTime() == endDate.getTime()) {
            
            myDate =  new Date(startDate.getTime());
            
        } else {
            
            // Get a random number between the start date and end date
            long randomDateLong = RandomGenerators.getRandom(startDate.getTime(),
                    endDate.getTime(), rand);
            myDate = new Date(randomDateLong);
            
        }
        
        return myDate;
        
    }
    
    /**
     * Returns a Date, offset from the present system time offset by the number
     * of milliseconds requested. Millis can be positive (and the Date will be
     * in the future) or negative (and the Date will be in the past).
     * @param offset The amount to offset the generated Date from the current
     * system time.  Can be positive or negative.
     * @return The Date requested.
     */
    public static Date getDateOffsetFromNow(long offset) {
        
        Date myDate;
        
        if (offset == 0) {
            
            myDate =  new Date();
            
        } else {
            myDate = new Date(System.currentTimeMillis() + offset);
        }
        
        return myDate;
        
    }
    
    /**
     * Takes a number of days and returns the equivalent number of milliseconds.
     * <P>
     * NOTE: Negative values still produce positive results.
     * @param numDays The number of days.
     * @return The number of milliseconds the provided number of days
     * represents.
     */
    public static long calcDaysAsMillis(long numDays) {
        
        if (numDays > 0) {
            
            numDays = numDays * -1; // ...make it positive
            
        }
        
        // days -> hours(24) -> mins(60) -> secs(60) -> millis(1000)
        return numDays * 24L * 60L * 60L * 1000L;
        
    }
    
    /**
     * Get a new date, based on the startDate provided with numDays in the past 
     * or future.
     * <P>
     * This is a non destructive method.  It does not alter the Date object
     * passed as a parameter.
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the Date
     * argument is null.
     * @param startFrom The Date to subtract the days from.
     * @param numDays The number of days to move.  Positive values return days 
     * in the future, negative values return days in the past.
     * @return The resulting Date.
     * @throws IllegalArgumentException If the Date object provided is NULL.
     */
    public static Date moveDays(Date startFrom, int numDays) {
        
        Date myDate;
        
        if (startFrom == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "startFrom");
            throw new IllegalArgumentException(NULL_PARAMETER_EX
                    + "startFrom");
            
        }
        
        if (numDays == 0) {
            
            myDate = new Date(startFrom.getTime());
            
        } else {
            
            myDate = new Date(startFrom.getTime() - calcDaysAsMillis(numDays));
            
        }
        
        return myDate;
        
    }
    
    /**
     * Generates a List of Date objects which range over a specified period
     * of time. They begin at the start Date provided, and the remaining
     * occur evenly spread over the duration.  If one instant point is requested
     * then the reurned List will be of size(1) and this item will be a Date
     * the same as the provided start date (i.e. the duration will be ignored).
     * <P>
     * Nullsafe - An IllegalArgumentException will be thrown if the start Date
     * is null or the number of instant points requested is <= 0.
     * @param aStartDate The Date for the first Date in the list.
     * @param millisDuration The duration across which the Dates must be
     * ranged.
     * @param noOfInstantPoints The number of Dates to generate between the
     * start Date over the period requested.
     * @return The list of Dates as requested.
     * @throws IllegalArgumentException If the Date object provided is NULL or
     * the noOfInstantPoints is <= 0
     */
    public static List<Date> createDateRangeList(Date aStartDate,
            long millisDuration, int noOfInstantPoints) {
        
        List<Date> myDateRangeList;
        
        if (aStartDate == null) {
            
            LOGGER.log(Level.WARNING, NULL_PARAMETER_EX + "aStatDate");
            throw new IllegalArgumentException(NULL_PARAMETER_EX
                    + "aStartDate");
            
        } else if (noOfInstantPoints <= 0) {
            
            LOGGER.log(Level.WARNING, "The number of requested instant points"
                    + " was <= 0: " + noOfInstantPoints + ". Throwing an "
                    + "IllegalArgumentException.");
            throw new IllegalArgumentException("The number of requested " +
                    "instant points was <= 0: " + noOfInstantPoints);
            
        }
        
        myDateRangeList = new ArrayList<Date>();
        
        if (noOfInstantPoints == 1) {
        
            myDateRangeList.add(new Date(aStartDate.getTime()));
            
        } else {
            
            // Calculate how long the duration between each instant in the range
            // should be
            long intraInstantDuration = millisDuration / (noOfInstantPoints - 1);
            
            // Create the instants and put them in the list
            long currentInstantMillis = aStartDate.getTime();
            
            for (int i = 0 ; i < noOfInstantPoints ; i ++) {
                
                // Create and store our latest instant
                Date currentInstant = new Date(currentInstantMillis);
                myDateRangeList.add(currentInstant);
                
                // Increment our instant counter
                currentInstantMillis += intraInstantDuration;
                
            }
        }
        
        return myDateRangeList;
        
    }
}